
/*
 * Android Bug Reporter
 * 
 *  !! This source code file is NOT licensed under MIT License !!
 * 
 * Copyright (c) 20101 Kevin Yuan (farproc@gmail.com)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * 
 **/

package com.farproc.android;

import java.lang.Thread.UncaughtExceptionHandler;
import java.util.List;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;

/**
 * Interface between applications and the Bug Reporter Library.
 * 
 * @author Kevin Yuan
 *
 */
public class BugReporter {
	/** Action to start the bug reporter activity. */
	public static final String ACTION_SHOW = "com.farproc.android.bugreporter.action.SHOW";
	/** Application name in String. */
	public static final String EXTRA_APP_NAME = "com.farproc.android.bugreporter.extra.APP_NAME";
	/** Application package in String. */
	public static final String EXTRA_APP_PACKAGE = "com.farproc.android.bugreporter.extra.APP_PACKAGE";
	/** Application description in String. */
	public static final String EXTRA_APP_DESCRIPTION = "com.farproc.android.bugreporter.extra.APP_DESCRIPTION";
	/** Application version in int. */
	public static final String EXTRA_APP_VERSION_CODE = "com.farproc.android.bugreporter.extra.APP_VERSION_CODE";
	/** Application version in String. */
	public static final String EXTRA_APP_VERSION_NAME = "com.farproc.android.bugreporter.extra.APP_VERSION_NAME";
	/** Package signature in String. */
	public static final String EXTRA_PACKAGE_SIGNATURES = "com.farproc.android.bugreporter.extra.PACKAGE_SIGNATURES";
	/** Application icon in Bitmap. */
	public static final String EXTRA_APP_ICON = "com.farproc.android.bugreporter.extra.APP_ICON";
	/** Exception object. What happened? */
	public static final String EXTRA_EXCEPTION = "com.farproc.android.bugreporter.extra.EXCEPTION";
	/** String. In which thread? */
	public static final String EXTRA_THREAD = "com.farproc.android.bugreporter.extra.THREAD";
	/** String. */
	public static final String EXTRA_RECEIVER_EMAIL = "com.farproc.android.bugreporter.extra.RECEIVER_EMAIL";
	/** String. Subject of the bug report email. "App Bug Report" by default. */
	public static final String EXTRA_EMAIL_SUBJECT = "com.farproc.android.bugreporter.extra.EMAIL_SUBJECT";
	
	private static final String BUG_REPORTER_PACKAGE = "com.farproc.android.bugreporter";
	
	/**
	 * Check Whether the Bug Reporter Library is installed.
	 * @param ctx Context of this application.
	 * @return true if installed otherwise false. 
	 */
	public static boolean isInstalled(final Context ctx) {
		final Intent bpIntent = new Intent(ACTION_SHOW);
		final List<ResolveInfo> info = ctx.getPackageManager().queryIntentActivities(bpIntent, PackageManager.MATCH_DEFAULT_ONLY);
		return info.size() > 0;
	}
	
	/**
	 * Install Bug Reporter Library from Android Market.
	 * @param ctx
	 */
	public static void installFromMarket(final Context ctx) {
		final Uri marketUri = Uri.parse("market://search?q=pname:" + BUG_REPORTER_PACKAGE);
	    final Intent marketIntent = new Intent(Intent.ACTION_VIEW).setData(marketUri);
	    ctx.startActivity(marketIntent);
	}
	
	/**
	 * 
	 * @author Kevin Yuan
	 *
	 */
	private static class UncaughtHandler implements UncaughtExceptionHandler {
		private String mReceiverEmail;
		private String mEmailSubject;
		private Class<?> mActivityToStartWhenNotInstalled;
		private Context mCtx;
		private Handler mUIThreadHandler;
		private long mUIThreadId;
		
		public UncaughtHandler(final Context ctx, 
				final Handler UIThreadHandler, final long UIThreadId,
				final String receiverEmail, final String emailSubject,
				final Class<?> toStartWhenNotInstalled) {
			super();
			mCtx = ctx;
			mReceiverEmail = receiverEmail;
			mEmailSubject = emailSubject;
			mActivityToStartWhenNotInstalled = toStartWhenNotInstalled;
			mUIThreadHandler = UIThreadHandler;
			mUIThreadId = UIThreadId;
			
		}
		
		private void performUncaught(Thread thread, Throwable ex) {
			if(!isInstalled(mCtx)) {
				if(mActivityToStartWhenNotInstalled != null) {
					final Intent notInstalled = new Intent(mCtx, mActivityToStartWhenNotInstalled);
					notInstalled.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
					mCtx.startActivity(notInstalled);
				} else {
					Thread.getDefaultUncaughtExceptionHandler().uncaughtException(thread, ex);
					return;
				}
			} else {
				final PackageManager pm = mCtx.getPackageManager();
				final String packageName = mCtx.getPackageName();
				ApplicationInfo appInfo = null;
				PackageInfo packInfo = null;
				try {
					appInfo = pm.getApplicationInfo(packageName, 0);
					packInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
				} catch (NameNotFoundException e) {
					throw new RuntimeException(e);
				}
				
				final Intent report = new Intent(ACTION_SHOW);
				final Drawable iconDrawable = appInfo.loadIcon(pm);
				final Bitmap iconBitmap = iconDrawable instanceof BitmapDrawable 
					? ((BitmapDrawable)iconDrawable).getBitmap()
					: null;
				report.putExtra(EXTRA_APP_NAME, appInfo.loadLabel(pm))
				      .putExtra(EXTRA_APP_DESCRIPTION, appInfo.loadDescription(pm))
				      .putExtra(EXTRA_APP_ICON, iconBitmap)
				      .putExtra(EXTRA_APP_PACKAGE, packageName)
				      .putExtra(EXTRA_APP_VERSION_CODE, packInfo.versionCode)
				      .putExtra(EXTRA_APP_VERSION_NAME, packInfo.versionName)
				      .putExtra(EXTRA_THREAD, thread.toString())
				      .putExtra(EXTRA_EXCEPTION, ex);
				
				final StringBuilder sbSigs = new StringBuilder();
				for(Signature sig:packInfo.signatures) {
					sbSigs.append(sig.hashCode()).append(";");
				}
				report.putExtra(EXTRA_PACKAGE_SIGNATURES, sbSigs.toString());
				report.putExtra(EXTRA_RECEIVER_EMAIL, mReceiverEmail);
				report.putExtra(EXTRA_EMAIL_SUBJECT, mEmailSubject);
				
				report.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
				
				try {
					mCtx.startActivity(report);
				} catch(Exception e) {
					Log.e("ee", e.toString());
				}
			}
			// Terminate.
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {

			}
			java.lang.System.exit(0);
		}
		
		@Override
		public void uncaughtException(final Thread thread, final Throwable ex) {
			if(Thread.currentThread().getId() == mUIThreadId) {
				// This is the main(UI) thread, call performUncaught directly.
				// Handlers of main thread are all broken now.
				performUncaught(thread, ex);
			} else {
				// This is a worker thread.
				mUIThreadHandler.post(new Runnable() {
					
					@Override
					public void run() {
						performUncaught(thread, ex);
						
					}
				});
			}
		}
	}
	
	/**
	 * Start monitoring of uncaught exceptions. 
	 * Normally, this method is used in a worker thread(a thread other than the main thread).
	 * @see #setupMonitor(Context, String, String, Class<?>) 
	 * 
	 * @param ctx The Context of this application.
	 * @param UIThreadHandler A Handler object created in main(UI) thread.
	 * @param UIThreadId The ID of main(UI) thread.
	 * @param receiverEmail A EMail address where the bug reports sent to.
	 * @param emailSubject Customized subject of bug reporting. null for default subject "[app] Bug Report"
	 * @param activityToStartWhenNotInstalled <b>(Not stable yet! Do not use !!)</b> The Class object of an Activity which will be shown when an exception caught 
	 * and the Bug Reporter Library is not installed.
	 * You can prompt the user to install Bug Reporter Library or tell him anything you want.
	 * null for default behavior(Force Close dialog). 
	 */
	public static void setupMonitor(final Context ctx, 
			final Handler UIThreadHandler, final long UIThreadId,
			final String receiverEmail, final String emailSubject,
			final Class<?> activityToStartWhenNotInstalled) {
		final Thread currentThread = Thread.currentThread();
		final UncaughtExceptionHandler oldHandler = currentThread.getUncaughtExceptionHandler();
		if(!(oldHandler instanceof UncaughtHandler)) {
			currentThread.setUncaughtExceptionHandler(
					new UncaughtHandler(ctx, 
							UIThreadHandler, UIThreadId,
							receiverEmail, emailSubject,
							activityToStartWhenNotInstalled));
		} else {
			throw new IllegalStateException("Moniter allready setup!");
		}
	}
	
	/**
	 * Start monitoring of uncaught exceptions. 
	 * Normally, this method is used in the main thread.
	 * 
	 * @see #setupMonitor(Context, Handler, long, String, String, Class<?>)
	 */
	public static void setupMonitor(final Context ctx,
			final String receiverEmail, final String emailSubject,
			final Class<?> activityToStartWhenNotInstalled) {
		setupMonitor(ctx, 
				new Handler(), Thread.currentThread().getId(), 
				receiverEmail, emailSubject, activityToStartWhenNotInstalled);
	}
	
}
